package com.cloud.rou.im.server;


import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import jdk.nashorn.internal.ir.annotations.Immutable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;


/**
 * <p></p>
 *
 * @author GaoShuangWen
 * @since 2019-11-15 19:00
 */
public final class Retryer<V> {
    private final StopStrategy stopStrategy;
    private final WaitStrategy waitStrategy;
    private final BlockStrategy blockStrategy;
    private final AttemptTimeLimiter<V> attemptTimeLimiter;
    private final Predicate<Attempt<V>> rejectionPredicate;
    private final Collection<RetryListener> listeners;

    public Retryer( StopStrategy stopStrategy,  WaitStrategy waitStrategy,  Predicate<Attempt<V>> rejectionPredicate) {
        this(AttemptTimeLimiters.noTimeLimit(), stopStrategy, waitStrategy, BlockStrategies.threadSleepStrategy(), rejectionPredicate);
    }

    public Retryer( AttemptTimeLimiter<V> attemptTimeLimiter,  StopStrategy stopStrategy,  WaitStrategy waitStrategy,  Predicate<Attempt<V>> rejectionPredicate) {
        this(attemptTimeLimiter, stopStrategy, waitStrategy, BlockStrategies.threadSleepStrategy(), rejectionPredicate);
    }

    public Retryer( AttemptTimeLimiter<V> attemptTimeLimiter,  StopStrategy stopStrategy,  WaitStrategy waitStrategy,  BlockStrategy blockStrategy,  Predicate<Attempt<V>> rejectionPredicate) {
        this(attemptTimeLimiter, stopStrategy, waitStrategy, blockStrategy, rejectionPredicate, new ArrayList());
    }

    @Beta
    public Retryer( AttemptTimeLimiter<V> attemptTimeLimiter,  StopStrategy stopStrategy,  WaitStrategy waitStrategy,  BlockStrategy blockStrategy,  Predicate<Attempt<V>> rejectionPredicate,  Collection<RetryListener> listeners) {
        Preconditions.checkNotNull(attemptTimeLimiter, "timeLimiter may not be null");
        Preconditions.checkNotNull(stopStrategy, "stopStrategy may not be null");
        Preconditions.checkNotNull(waitStrategy, "waitStrategy may not be null");
        Preconditions.checkNotNull(blockStrategy, "blockStrategy may not be null");
        Preconditions.checkNotNull(rejectionPredicate, "rejectionPredicate may not be null");
        Preconditions.checkNotNull(listeners, "listeners may not null");
        this.attemptTimeLimiter = attemptTimeLimiter;
        this.stopStrategy = stopStrategy;
        this.waitStrategy = waitStrategy;
        this.blockStrategy = blockStrategy;
        this.rejectionPredicate = rejectionPredicate;
        this.listeners = listeners;
    }

    public V call(Callable<V> callable) throws ExecutionException, RetryException {
        long startTime = System.nanoTime();
        int attemptNumber = 1;

        while(true) {
            Object attempt;
            try {
                V result = this.attemptTimeLimiter.call(callable);
                attempt = new Retryer.ResultAttempt(result, (long)attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            } catch (Throwable var9) {
                attempt = new Retryer.ExceptionAttempt(var9, (long)attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            }

            Iterator i$ = this.listeners.iterator();

            while(i$.hasNext()) {
                RetryListener listener = (RetryListener)i$.next();
                listener.onRetry((Attempt)attempt);
            }

            if (!this.rejectionPredicate.apply((Attempt<V>) attempt)) {
                return (V) ((Attempt)attempt).get();
            }

            if (this.stopStrategy.shouldStop((Attempt)attempt)) {
                throw new RetryException(attemptNumber, (Attempt)attempt);
            }

            long sleepTime = this.waitStrategy.computeSleepTime((Attempt)attempt);

            try {
                this.blockStrategy.block(sleepTime);
            } catch (InterruptedException var10) {
                Thread.currentThread().interrupt();
                throw new RetryException(attemptNumber, (Attempt)attempt);
            }

            ++attemptNumber;
        }
    }

    public Retryer.RetryerCallable<V> wrap(Callable<V> callable) {
        return new Retryer.RetryerCallable(this, callable);
    }

    public static class RetryerCallable<X> implements Callable<X> {
        private Retryer<X> retryer;
        private Callable<X> callable;

        private RetryerCallable(Retryer<X> retryer, Callable<X> callable) {
            this.retryer = retryer;
            this.callable = callable;
        }

        public X call() throws ExecutionException, RetryException {
            return this.retryer.call(this.callable);
        }
    }

    @Immutable
    static final class ExceptionAttempt<R> implements Attempt<R> {
        private final ExecutionException e;
        private final long attemptNumber;
        private final long delaySinceFirstAttempt;

        public ExceptionAttempt(Throwable cause, long attemptNumber, long delaySinceFirstAttempt) {
            this.e = new ExecutionException(cause);
            this.attemptNumber = attemptNumber;
            this.delaySinceFirstAttempt = delaySinceFirstAttempt;
        }

        public R get() throws ExecutionException {
            throw this.e;
        }

        public boolean hasResult() {
            return false;
        }

        public boolean hasException() {
            return true;
        }

        public R getResult() throws IllegalStateException {
            throw new IllegalStateException("The attempt resulted in an exception, not in a result");
        }

        public Throwable getExceptionCause() throws IllegalStateException {
            return this.e.getCause();
        }

        public long getAttemptNumber() {
            return this.attemptNumber;
        }

        public long getDelaySinceFirstAttempt() {
            return this.delaySinceFirstAttempt;
        }
    }

    static final class ResultAttempt<R> implements Attempt<R> {
        private final R result;
        private final long attemptNumber;
        private final long delaySinceFirstAttempt;

        public ResultAttempt(R result, long attemptNumber, long delaySinceFirstAttempt) {
            this.result = result;
            this.attemptNumber = attemptNumber;
            this.delaySinceFirstAttempt = delaySinceFirstAttempt;
        }

        public R get() throws ExecutionException {
            return this.result;
        }

        public boolean hasResult() {
            return true;
        }

        public boolean hasException() {
            return false;
        }

        public R getResult() throws IllegalStateException {
            return this.result;
        }

        public Throwable getExceptionCause() throws IllegalStateException {
            throw new IllegalStateException("The attempt resulted in a result, not in an exception");
        }

        public long getAttemptNumber() {
            return this.attemptNumber;
        }

        public long getDelaySinceFirstAttempt() {
            return this.delaySinceFirstAttempt;
        }
    }
}